﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using BookSleeve;
using Codeplex.Data.Internal;
using Codeplex.Data.SerializeProvider;

namespace Codeplex.Data
{
    public class HashMapper
    {
        public static HashMapperConfig DefaultConfig { get; set; }

        static HashMapper()
        {
            // TODO:Read from Web.config
            DefaultConfig = new HashMapperConfig(0, "", () => new DataContractJsonSerializeProvider());
        }
    }

    public class HashMapper<TObjectType>
    {
        static KeyIndexed<string, CompiledAccessor> accessor = AccessorCache.Lookup(typeof(TObjectType));

        readonly IHashCommands hashCommands;
        readonly IKeyCommands keyCommands;
        readonly int db;
        readonly string id;
        readonly string key;
        readonly ISerializeProvider serializeProvider;

        public string Key { get { return key; } }

        public HashMapper(RedisConnection redisConnection, object id)
            : this(redisConnection, id, HashMapper.DefaultConfig)
        {
        }

        public HashMapper(RedisConnection redisConnection, object id, HashMapperConfig config)
        {
            this.hashCommands = redisConnection;
            this.keyCommands = redisConnection;
            this.id = id.ToString();
            this.db = config.Db;
            this.serializeProvider = config.SerializeProviderFactory();
            this.key = config.KeyPrefix + typeof(TObjectType).Name + ":" + id;
        }

        public Task<TObjectType> Get(bool queueJump = false)
        {
            var dict = hashCommands.GetAll(db, key, queueJump);
            return dict.ContinueWith(t => MapHelper.MapToObject<TObjectType>(serializeProvider, t.Result, accessor));
        }

        public Task<TFieldType> GetField<TFieldType>(Expression<Func<TObjectType, TFieldType>> fieldSelector, bool queueJump = false)
        {
            return GetField<TFieldType>(ExtractFieldName(fieldSelector), queueJump);
        }

        public Task<TFieldType> GetField<TFieldType>(string field, bool queueJump = false)
        {
            return hashCommands.Get(db, key, field, queueJump)
                .ContinueWith(t => (TFieldType)serializeProvider.Deserialize(typeof(TFieldType), t.Result));
        }

        public Task Set(TObjectType obj, bool queueJump = false)
        {
            var values = accessor.ToDictionary(
                x => x.MemberName,
                x => serializeProvider.Serialize(x.MemberType, x.GetValueDirect(obj)));
            return hashCommands.Set(db, key, values, queueJump);
        }

        public Task Set(TObjectType obj, TimeSpan expire, bool queueJump = false)
        {
            // TODO:Transaction?
            return Set(obj, queueJump)
                .ContinueWith(t => keyCommands.Expire(db, key, expire.Seconds, queueJump));
        }

        public Task SetField<TFieldType>(Expression<Func<TObjectType, TFieldType>> fieldSelector, TFieldType value, bool queueJump = false)
        {
            return SetField<TFieldType>(ExtractFieldName(fieldSelector), value, queueJump);
        }

        public Task SetField<TFieldType>(string field, TFieldType value, bool queueJump = false)
        {
            return hashCommands.Set(db, key, field, serializeProvider.Serialize(typeof(TFieldType), value), queueJump);
        }

        // TODO:SetIfNotExists
        // TODO:Sets

        public Task<TObjectType> GetOrAdd(Func<TObjectType> objectFactory, bool queueJump = false)
        {
            return Get(queueJump).ContinueWith(t =>
            {
                if (t.Result != null) return t;

                var obj = objectFactory();
                return Set(obj, queueJump).ContinueWith(_ => obj);
            }).Unwrap();
        }

        public Task<TObjectType> GetOrAdd(Func<TObjectType> objectFactory, TimeSpan expire, bool queueJump = false)
        {
            return Get(queueJump).ContinueWith(t =>
            {
                if (t.Result != null) return t;

                var obj = objectFactory();
                return Set(obj, expire, queueJump).ContinueWith(_ => obj);
            }).Unwrap();
        }

        public Task<long> Increment<TField>(Expression<Func<TObjectType, TField>> fieldSelector, int value = 1, bool queueJump = false)
        {
            var field = ExtractFieldName(fieldSelector);
            return hashCommands.Increment(db, key, field, value, queueJump);
        }

        public Task<long> Decrement<TField>(Expression<Func<TObjectType, TField>> fieldSelector, int value = 1, bool queueJump = false)
        {
            return Increment(fieldSelector, -value, queueJump);
        }

        public Task<bool> Exists(bool queueJump = false)
        {
            return keyCommands.Exists(db, key, queueJump);
        }

        public Task<bool> ExistsField(string field, bool queueJump = false)
        {
            return hashCommands.Exists(db, key, field, queueJump);
        }

        public Task<bool> Remove(bool queueJump = false)
        {
            return keyCommands.Remove(db, key, queueJump);
        }

        string ExtractFieldName<TField>(Expression<Func<TObjectType, TField>> fieldSelector)
        {
            // TODO:Fix for other expression
            var memberExpr = fieldSelector.Body as MemberExpression;
            if (memberExpr == null) throw new ArgumentException("fieldSelector must be MemberExpression", "fieldSelector");
            return memberExpr.Member.Name;
        }
    }
}